package com.gframework.mybatis.dao.mybatis.provider.core;

import java.util.HashMap;
import java.util.Map;

import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Lang;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.PropertyParser;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;

/**
 * 用于增强provider的sql驱动器，其主要功能就是针对ProviderSqlSource进行增强.
 * 例如可以在provider中扩展新的{@code key-value}参数，这个是mybatis没有提供的功能.
 * 
 * <pre>
 * 如果要使用此功能，首先你需要在使用 Provider相关注解的同时配置{@code @Lang(ProviderPlusLanguageDriver.class)}。
 * 从而指定sql驱动器。这样一来，你就可以在你的Provider生成器中调用：
 * 1、{@link ProviderPlusLanguageDriver#setParam(Map)}
 * 2、{@link ProviderPlusLanguageDriver#addParam(String, Object)}
 * 3、{@link ProviderPlusLanguageDriver#clearParam()}
 * 来配置当前sql需要使用的额外参数。
 * </pre>
 * <p>
 * 本类仅存在一个线程相关问题，如果你非法的设置了参数，或者在设置参数后provider后续程序出现异常，那么设置的参数将不会被清除，而是一直保留到下一次被使用，
 * 这可能导致参数污染和内存溢出。
 * <br>
 * <strong>所以请确保你的provider不会出现异常，并且即使出现异常，也要调用{@link #clearParam()}方法来清除参数。</strong>
 * </p>
 * 
 * @since 1.0.0
 * @author Ghwolf
 * @since mybatis 3.5.3+
 * @since mybatis.spring.boot 2.1.1+
 * @see Lang
 * @see SelectProvider
 * @see UpdateProvider
 * @see DeleteProvider
 * @see InsertProvider
 */
public class ProviderPlusLanguageDriver extends XMLLanguageDriver {

	/**
	 * 存放provider在生成sql时额外的参数
	 */
	private static final ThreadLocal<Map<String, Object>> PROVIDER_PARAM = new ThreadLocal<>();

	/**
	 * 设置一个新的参数对集合，这将会覆盖之前设置的所有参数
	 * 
	 * @param param 参数集合对象
	 */
	public static void setParam(Map<String, Object> param) {
		PROVIDER_PARAM.set(param);
	}

	/**
	 * 添加一个新的参数.
	 * 
	 * <pre>
	 * 请保证在sql构建过程中不会出现异常，否则设置的参数将会被一直保留甚至在下一次sql执行中被使用，者将会产生一个不安全的因素。
	 * 你可以使用try ... catch 来保证在抛出异常时，调用{@link #clearParam()} 方法来清空参数。
	 * 
	 * 或者另一个比较安全的做法就是在sql彻底生成完毕后调用 {@link #setParam(Map)} 方法。
	 * </pre>
	 * 
	 * @param key 参数key
	 * @param value 参数值
	 * @see #setParam(Map)
	 */
	public static void addParam(String key, Object value) {
		Map<String,Object> map = PROVIDER_PARAM.get();
		if (map == null) {
			map = new HashMap<>();
			PROVIDER_PARAM.set(map);
		}
		map.put(key, value);
	}

	/**
	 * 清空参数
	 */
	public static final void clearParam() {
		PROVIDER_PARAM.remove();
	}

	@Override
	public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
		Map<String, Object> map = PROVIDER_PARAM.get();
		clearParam();
		script = PropertyParser.parse(script, configuration.getVariables());
		return new AddParamRawSqlSource(configuration, script, parameterType, map);
	}

}
